﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using System.Xml.Serialization;

namespace ProjectCastamere
{
    class EffectClass
    {
        // Just a simple shader effect( if needed )
        private BasicEffect basicEffect;

        public BasicEffect BasicEffect
        {
            get { return basicEffect; }
        }

        // For custom shaders
        private Effect effect;

        public Effect Effect
        {
            get { return effect; }
        }

        // If needed post processing
        private Effect postProcessEffect;

        public Effect PostProcessEffect
        {
            get { return postProcessEffect; }
        }

        // setup render targets
        private RenderTarget2D sceneRenderTarget;
        private RenderTarget2D normalRenderTarget;

        //default constuctor
        public EffectClass(GraphicsDevice device)
        {
            basicEffect = new BasicEffect(device);
        }

        // override constructor(use this for custom effects)
        public EffectClass(Application app, Model model, string effectName, bool postProcess)
        {
            effect = app.Content.Load <Effect>("Effects/" + effectName);

            if (postProcess)
            {
                postProcessEffect = app.Content.Load<Effect>("Effects/PostProcess");
            }

                // set up render targets
                PresentationParameters pp = app.GraphicsDevice.PresentationParameters;

                sceneRenderTarget = new RenderTarget2D(app.GraphicsDevice,
                                                   pp.BackBufferWidth, pp.BackBufferHeight, false,
                                                   pp.BackBufferFormat, pp.DepthStencilFormat);

                normalRenderTarget = new RenderTarget2D(app.GraphicsDevice,
                                                         pp.BackBufferWidth, pp.BackBufferHeight, false,
                                                         pp.BackBufferFormat, pp.DepthStencilFormat);
                // Use the current effect 
                ChangeEffectUsedByModel(model, effect);
        }

        /// <summary>
        /// Helper class, mainly to allow us to switch effects at run time
        /// if we wanted to
        /// </summary>
        /// <param name="model"></param>
        /// <param name="replacementEffect"></param>
        static void ChangeEffectUsedByModel(Model model, Effect replacementEffect)
        {
            // Table mapping the original effects to our replacement versions.
            Dictionary<Effect, Effect> effectMapping = new Dictionary<Effect, Effect>();

            foreach (ModelMesh mesh in model.Meshes)
            {
                // Scan over all the effects currently on the mesh.
                foreach (BasicEffect oldEffect in mesh.Effects)
                {
                    // If we haven't already seen this effect...
                    if (!effectMapping.ContainsKey(oldEffect))
                    {
                        // Make a clone of our replacement effect. We can't just use
                        // it directly, because the same effect might need to be
                        // applied several times to different parts of the model using
                        // a different texture each time, so we need a fresh copy each
                        // time we want to set a different texture into it.
                        Effect newEffect = replacementEffect.Clone();

                        // Copy across the texture from the original effect.
                        newEffect.Parameters["Texture"].SetValue(oldEffect.Texture);
                        newEffect.Parameters["TextureEnabled"].SetValue(oldEffect.TextureEnabled);

                        effectMapping.Add(oldEffect, newEffect);
                    }
                }

                // Now that we've found all the effects in use on this mesh,
                // update it to use our new replacement versions.
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    meshPart.Effect = effectMapping[meshPart.Effect];
                }
            }
        }

        /// <summary>
        /// Simply draws the model with a very basic texture shader with deafult lighting
        /// </summary>
        /// <param name="app">The application</param>
        /// <param name="model">The model to be drawn</param>
        /// <param name="view">The view matrix</param>
        /// <param name="projection">The projection matrix</param>
        /// <param name="scale">the scalar</param>
        public static void DrawModelWithBasicEffect(Application app, Model model, Matrix view, Matrix projection, Matrix scale)
        {
            Matrix[] transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.View = view;
                    effect.Projection = projection;
                    effect.World = transforms[mesh.ParentBone.Index] * scale;
                }
                mesh.Draw();
            }
        }

        /// <summary>
        /// Draws the model with solely an ambient light
        /// </summary>
        /// <param name="app">The application</param>
        /// <param name="model">The model to be drawn</param>
        /// <param name="view">The view Matrix</param>
        /// <param name="projection">The projection Matrix</param>
        /// <param name="scale">The scalar</param>
        /// <param name="ambientColour">The ambient colour</param>
        /// <param name="ambientIntensity">The ambient intensity</param>
        public void DrawModelWithCustomEffect(Application app, Model model, Matrix view, Matrix projection, Matrix scale, Vector4 ambientColour, float ambientIntensity)
        {
            Matrix[] transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    part.Effect = effect;
                    effect.Parameters["World"].SetValue(transforms[mesh.ParentBone.Index] * scale);
                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                    effect.Parameters["AmbientColor"].SetValue(ambientColour);
                    effect.Parameters["AmbientIntensity"].SetValue(ambientIntensity);
                }
                mesh.Draw();
            }
        }

        /// <summary>
        /// The following code draws a model with a toon shading effect and edge detection
        /// </summary>
        /// <param name="app"></param>
        /// <param name="model"></param>
        /// <param name="view"></param>
        /// <param name="projection"></param>
        /// <param name="scale"></param>
        public void DrawModelWithCustomEffect(Application app, Model model, Matrix view, Matrix projection, Matrix scale)
        {
            GraphicsDevice device = app.GraphicsDevice;

            // If we are doing edge detection, first off we need to render the
            // normals and depth of our model into a special rendertarget.
            device.SetRenderTarget(normalRenderTarget);
            device.Clear(Color.Black);
            DrawModel(app, model, view, projection, scale, "NormalDepth");

            // If we are doing edge detection we
            // need to draw the model into a special rendertarget which can then be
            // fed into the postprocessing shader. Otherwise can just draw it
            // directly onto the backbuffer.
            device.SetRenderTarget(sceneRenderTarget);
            device.Clear(Color.CornflowerBlue);

            // Draw the model, using the toon shading technique.
            string effectTechniqueName;

            effectTechniqueName = "Toon";

            // Draw the model with this effect
            DrawModel(app, model, view, projection, scale, effectTechniqueName);

            // Run the postprocessing filter over the scene that we just rendered.
            device.SetRenderTarget(null);

            ApplyPostProcess(app);
        }

        // Helper class to draw model
        private void DrawModel(Application app, Model model, Matrix view, Matrix projection, Matrix scale, string effectTechniqueName)
        {
            // Set suitable renderstates for drawing a 3D model.
            app.GraphicsDevice.BlendState = BlendState.Opaque;
            app.GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            // Look up the bone transform matrices.
            Matrix[] transforms = new Matrix[model.Bones.Count];

            model.CopyAbsoluteBoneTransformsTo(transforms);

            // Draw the model.
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    // Specify which effect technique to use.
                    effect.CurrentTechnique = effect.Techniques[effectTechniqueName];

                    Matrix localWorld = transforms[mesh.ParentBone.Index] * scale;

                    effect.Parameters["World"].SetValue(localWorld);
                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                }

                mesh.Draw();
            }
        }

        // Apply post processing
        void ApplyPostProcess(Application app)
        {
            string techniqueName;

            EffectParameterCollection parameters = postProcessEffect.Parameters;

            Vector2 resolution = new Vector2(sceneRenderTarget.Width,
                                                sceneRenderTarget.Height);

            Texture2D normalDepthTexture = normalRenderTarget;

            parameters["EdgeWidth"].SetValue(1.0f); // change these settings to affect stuff
            parameters["EdgeIntensity"].SetValue(1.0f); // change these to affect stuff
            parameters["ScreenResolution"].SetValue(resolution);
            parameters["NormalDepthTexture"].SetValue(normalDepthTexture);

            techniqueName = "EdgeDetect";
            
            // Activate the appropriate effect technique.
            postProcessEffect.CurrentTechnique = postProcessEffect.Techniques[techniqueName];

            // Draw a fullscreen sprite to apply the postprocessing effect.
            SpriteBatch spriteBatch = new SpriteBatch(app.GraphicsDevice);
            spriteBatch.Begin(0, BlendState.Opaque, null, null, null, postProcessEffect);
            spriteBatch.Draw(sceneRenderTarget, Vector2.Zero, Color.White);
            spriteBatch.End();
        }
    }
}
